"""
Configuration system for continual learning experiments.
"""

import os
import copy
from typing import Dict, Any, Optional, Union
import yaml


class Config:
    """
    Configuration class for managing experiment settings.

    This class provides functionality to:
    - Load configuration from YAML files
    - Access configuration options via attribute or dictionary syntax
    - Merge configurations from multiple sources
    - Save configuration to files

    Args:
        config_file (str, optional): Path to a YAML configuration file
        config_dict (dict, optional): Dictionary with configuration options
    """

    def __init__(
        self,
        config_file: Optional[str] = None,
        config_dict: Optional[Dict[str, Any]] = None,
    ):
        # Initialize with empty config
        self.config = {}

        # Load from file if provided
        if config_file is not None:
            self.load_from_file(config_file)

        # Update from dict if provided
        if config_dict is not None:
            self.update(config_dict)

    def __getitem__(self, key: str) -> Any:
        """Get configuration value by key."""
        return self.config.get(key)

    def __setitem__(self, key: str, value: Any) -> None:
        """Set configuration value by key."""
        self.config[key] = value

    def __contains__(self, key: str) -> bool:
        """Check if key exists in configuration."""
        return key in self.config

    def __repr__(self) -> str:
        """String representation of the configuration."""
        return f"Config with {len(self.config)} items"

    def get(self, key_path: str, default: Any = None) -> Any:
        """
        Get a configuration value by key path.

        Supports nested keys with dot notation, e.g., "model.backbone.type".

        Args:
            key_path (str): Key path with dot notation for nested keys
            default (Any): Default value if the key is not found

        Returns:
            Any: Configuration value or default
        """
        keys = key_path.split(".")
        value = self.config

        for key in keys:
            if isinstance(value, dict) and key in value:
                value = value[key]
            else:
                return default

        return value

    def set(self, key_path: str, value: Any) -> None:
        """
        Set a configuration value by key path.

        Supports nested keys with dot notation, e.g., "model.backbone.type".

        Args:
            key_path (str): Key path with dot notation for nested keys
            value (Any): Value to set
        """
        keys = key_path.split(".")
        current = self.config

        # Navigate to the innermost dict
        for key in keys[:-1]:
            if key not in current or not isinstance(current[key], dict):
                current[key] = {}
            current = current[key]

        # Set the value
        current[keys[-1]] = value

    def update(self, config_dict: Dict[str, Any]) -> None:
        """
        Update configuration with values from another dictionary.

        Args:
            config_dict (Dict[str, Any]): Dictionary with configuration values
        """

        # Helper function for recursive update
        def _update_dict(target, source):
            for key, value in source.items():
                if (
                    key in target
                    and isinstance(target[key], dict)
                    and isinstance(value, dict)
                ):
                    # If both are dictionaries, update recursively
                    _update_dict(target[key], value)
                else:
                    # Otherwise, replace the value
                    target[key] = value

        # Update the config
        _update_dict(self.config, config_dict)

    def load_from_file(self, file_path: str) -> None:
        """
        Load configuration from a YAML file.

        Args:
            file_path (str): Path to the YAML configuration file

        Raises:
            FileNotFoundError: If the file does not exist
            yaml.YAMLError: If the file contains invalid YAML
        """
        if not os.path.exists(file_path):
            raise FileNotFoundError(f"Configuration file not found: {file_path}")

        with open(file_path, "r") as file:
            try:
                config_dict = yaml.safe_load(file)
                # Update the current configuration
                self.update(config_dict)
            except yaml.YAMLError as e:
                raise yaml.YAMLError(f"Error parsing YAML file {file_path}: {str(e)}")

    def save_to_file(self, file_path: str) -> None:
        """
        Save configuration to a YAML file.

        Args:
            file_path (str): Path to save the configuration file

        Raises:
            IOError: If the file cannot be written
        """
        # Create directory if it doesn't exist
        os.makedirs(os.path.dirname(os.path.abspath(file_path)), exist_ok=True)

        with open(file_path, "w") as file:
            yaml.dump(self.config, file, default_flow_style=False)

    def copy(self) -> "Config":
        """
        Create a deep copy of this configuration.

        Returns:
            Config: New Config object with the same values
        """
        new_config = Config()
        new_config.config = copy.deepcopy(self.config)
        return new_config

    @property
    def as_dict(self) -> Dict[str, Any]:
        """Get configuration as a dictionary."""
        return copy.deepcopy(self.config)
